﻿using System.Collections.Generic;
using System.Linq;
using Duality;
using OpenTK;

namespace ClientApp
{
    public static class PlayerGestures
    {
        private static GestureGroup paper;
        private static GestureGroup rock;
        private static GestureGroup scissor;
        private static GestureGroup attack;
        private static GestureGroup retreat;
        private static GestureGroup hold;

        public static GestureGroup Paper
        {
            get { return paper; }
        }
        public static GestureGroup Rock
        {
            get { return rock; }
        }
        public static GestureGroup Scissor
        {
            get { return scissor; }
        }
        public static GestureGroup Attack
        {
            get { return attack; }
        }
        public static GestureGroup Retreat
        {
            get { return retreat; }
        }
        public static GestureGroup Hold
        {
            get { return hold; }
        }

        static PlayerGestures()
        {
            IEnumerable<Vector2> rectPts = new Vector2[] { 
                new Vector2(0.0f, 0.0f),
                new Vector2(1.0f, 0.0f),
                new Vector2(1.0f, 1.25f),
                new Vector2(0.0f, 1.25f),
                new Vector2(0.0f, 0.0f) };
            paper = new GestureGroup(30.0f, new[] { new Gesture(rectPts), new Gesture(rectPts.Reverse()) });

            IEnumerable<Vector2> rockPts = new Vector2[] { 
                new Vector2(0.0f, 1.0f),
                new Vector2(0.5f, 0.0f), 
                new Vector2(1.0f, 1.0f),
                new Vector2(1.5f, 0.0f), 
                new Vector2(2.0f, 1.0f)};
            rock = new GestureGroup(30.0f, new[] { new Gesture(rockPts), new Gesture(rockPts.Reverse()) });

            IEnumerable<Vector2> scissorPts = new Vector2[] { 
                new Vector2(0.0f, 0.0f),
                new Vector2(1.0f, 1.0f),
                new Vector2(0.0f, 1.0f),
                new Vector2(1.0f, 0.0f) };
            scissor = new GestureGroup(30.0f, new[] { new Gesture(scissorPts), new Gesture(scissorPts.Reverse()) });

            IEnumerable<Vector2> attackPts = new Vector2[] { 
                new Vector2(0.0f, 1.0f),
                new Vector2(0.0f, 0.0f) };
			attack = new GestureGroup(15.0f, new[] { new Gesture(attackPts) });

            IEnumerable<Vector2> retreatPts = new Vector2[] { 
                new Vector2(0.0f, 0.0f),
                new Vector2(0.0f, 1.0f) };
			retreat = new GestureGroup(15.0f, new[] { new Gesture(retreatPts) });

            IEnumerable<Vector2> holdPts = new Vector2[] { 
                new Vector2(0.0f, 0.0f),
                new Vector2(1.0f, 0.0f) };
			hold = new GestureGroup(15.0f, new[] { new Gesture(holdPts), new Gesture(holdPts.Reverse()) });
        }

        public static GestureGroup Match(Gesture other, float refAngle, out float bestScore)
        {
            var groups = new[] { rock, paper, scissor, attack, retreat, hold };
            bestScore = 0.01f;
            GestureGroup bestGroup = null;
            foreach (GestureGroup group in groups)
            {
                float score = group.Matches(other, refAngle);
                if (score > bestScore)
                {
                    bestScore = score;
                    bestGroup = group;
                }
            }
            if (bestGroup == null) bestScore = 0.0f;
            return bestGroup;
        }
    }

    public class GestureGroup
    {
        private List<Gesture> gestures;
        private float angleTolerance;
        private float minScore = 0.6f;
        private float maxScore = 0.9f;

        public float AngleTolerance
        {
            get { return this.angleTolerance; }
            set { this.angleTolerance = value; }
        }
        public float MinScore
        {
            get { return this.minScore; }
            set { this.minScore = value; }
        }
        public float MaxScore
        {
            get { return this.maxScore; }
            set { this.maxScore = value; }
        }

        public GestureGroup(float angleTolerance, IEnumerable<Gesture> gestures)
        {
            this.gestures = gestures.ToList();
            this.angleTolerance = angleTolerance;
        }

        public float Matches(Gesture other, float refAngle)
        {
            float bestScore = this.gestures.Max(g => g.Matches(other, refAngle - angleTolerance, refAngle + angleTolerance));
            return MathF.Clamp((bestScore - minScore) / (maxScore - minScore), 0.0f, 1.0f);
        }
    }

	public class Gesture
	{
		private static int SAMPLE_COUNT = 64;
		private List<Vector2> points = new List<Vector2>();
		private bool isPointerGesture = false;
		private float pointerAngle = 0.0f;
		private float pointerQuality = 0.0f;

		public Gesture() {}
		public Gesture(IEnumerable<Vector2> stroke)
		{
			this.Load(stroke);
		}

		public void Load(IEnumerable<Vector2> stroke)
		{
			points.Clear();
			points.AddRange(stroke);
			if (points.Count == 2)
			{
				isPointerGesture = true;
				pointerAngle = MathF.Angle(points[0].X, points[0].Y, points[1].X, points[1].Y);
				pointerQuality = 1.0f;
				points.Clear();
			}
			else
			{
				isPointerGesture = false;
				pointerAngle = MathF.Angle(points[0].X, points[0].Y, points[points.Count - 1].X, points[points.Count - 1].Y);
				
				float totalDist = 0.0f;
				float refDist = MathF.Max(MathF.Distance(points[0].X, points[0].Y, points[points.Count - 1].X, points[points.Count - 1].Y), 1.0f);
				for (int i = 0; i < points.Count; i++)
				{
					float dist = MathF.PointLineDistance(points[i].X, points[i].Y, points[0].X, points[0].Y, points[points.Count - 1].X, points[points.Count - 1].Y);
					totalDist += MathF.Abs(dist);
				}
				pointerQuality = 1.0f / (1.0f + 0.5f * (totalDist / refDist));
				pointerQuality *= MathF.Clamp(refDist / 100.0f, 0.0f, 1.0f); // Pointers that are too small get inaccurate

				Resample();
				NormalizeSizeAndPosition();
			}
		}
		public float Matches(Gesture other, float minAngle, float maxAngle, float angleStepSize = 2.0f)
		{
            if (!this.isPointerGesture && this.points.Count == 0) return 0.0f;
            if (!other.isPointerGesture && other.points.Count == 0) return 0.0f;
			

			if (this.isPointerGesture || other.isPointerGesture)
			{
				// Using radians in here
				minAngle = MathF.NormalizeAngle(MathF.DegToRad(minAngle));
				maxAngle = MathF.NormalizeAngle(MathF.DegToRad(maxAngle));

				float myPointerAngle = this.pointerAngle;
				float otherPointerAngle = other.pointerAngle;
				
				float idealPointerAngle = MathF.NormalizeAngle(myPointerAngle + (minAngle + maxAngle) * 0.5f);
				float idealAngleRange = MathF.Abs(maxAngle - minAngle) * 0.5f;
				float angleDiffTolerance = idealAngleRange;
				float angleToIdealDist = MathF.Max(0.0f, MathF.CircularDist(idealPointerAngle, otherPointerAngle) - idealAngleRange);
				float pointerScore = 1.0f - (angleToIdealDist / angleDiffTolerance);
				return MathF.Clamp(pointerScore * pointerQuality * other.pointerQuality, 0.0f, 1.0f);
			}

			float wtf = 0.5f * (-1.0f + MathF.Sqrt(5.0f));
			float x1 = wtf * minAngle + (1 - wtf) * maxAngle;
			float f1 = this.Distance(other, x1);
			float x2 = (1.0f - wtf) * minAngle + wtf * maxAngle;
			float f2 = this.Distance(other, x2);
			while (MathF.Abs(minAngle - maxAngle) > angleStepSize)
			{
				if (f1 < f2)
				{
					maxAngle = x2;
					x2 = x1;
					f2 = f1;
					x1 = wtf * minAngle + (1.0f - wtf) * maxAngle;
					f1 = this.Distance(other, x1);
				}
				else
				{
					minAngle = x1;
					x1 = x2;
					f1 = f2;
					x2 = wtf * maxAngle + (1.0f - wtf) * minAngle;
					f2 = this.Distance(other, x2);
				}
			}
			float bestDistance = MathF.Min(f1, f2);
			return 1.0f - bestDistance / (0.5f * MathF.Sqrt(2.0f));
		}
		
		private void Resample()
		{
            if (points.Count < 2)
            {
                points.Clear();
                return;
            }

			List<Vector2> normalizedPoints = new List<Vector2>();
			float meanDistance = 0.0f;
			for (int i = 1; i < points.Count; i++)
			{
				meanDistance += (points[i - 1] - points[i]).LengthFast;
			}
			meanDistance /= (SAMPLE_COUNT - 1);

			float distanceCarry = 0.0f;
			normalizedPoints.Add(points[0]);
			for (int i = 1; i < points.Count; i++)
			{
				Vector2 previous = points[i - 1];
				Vector2 current = points[i];
				float distanceCurrent = (current - previous).LengthFast;
				if (distanceCarry + distanceCurrent >= meanDistance)
				{
					float factor = (meanDistance - distanceCarry) / distanceCurrent;
					Vector2 newPoint = factor * (current - previous) + previous;

					normalizedPoints.Add(newPoint);
					points.Insert(i, newPoint);

					distanceCarry = 0.0f;
				}
				else
				{
					distanceCarry += distanceCurrent;
				}
			}

			points.Clear();
			points.AddRange(normalizedPoints);
		}
		private void NormalizeSizeAndPosition()
		{
			Vector2 min = new Vector2(float.PositiveInfinity, float.PositiveInfinity);
			Vector2 max = new Vector2(float.NegativeInfinity, float.NegativeInfinity);
			// find bounding box
			foreach (Vector2 point in this.points)
			{
				if (point.X < min.X) min.X = point.X;
				if (point.Y < min.Y) min.Y = point.Y;
				if (point.X > max.X) max.X = point.X;
				if (point.Y > max.Y) max.Y = point.Y;
			}
			Vector2 boundingBoxCenter = 0.5f * (min + max);
			float aspectRatio = 1.0f; //(max.X - min.X) / MathF.Max(0.0000001f, max.Y - min.Y); // Experimental: Maintain aspect ratio.
			// normalize
			for (int i = 0; i < this.points.Count; i++)
			{
				Vector2 point = this.points[i];
				point = (this.points[i] - boundingBoxCenter);
				point = new Vector2(point.X * aspectRatio / (max.X - min.X), point.Y / (max.Y - min.Y));
				this.points[i] = point;
			}
		}
		private float Distance(Gesture other, float angle)
		{
			int count = points.Count < other.points.Count ? points.Count : other.points.Count;
            angle = MathF.DegToRad(angle);
            List<Vector2> rotatedPoints = points.Take(count).Select(delegate(Vector2 v)
            {
                MathF.TransformCoord(ref v.X, ref v.Y, angle);
                return v;
            }).ToList();
			float distance = 0.0f;
			for (int i = 0; i < count; i++)
			{
				distance += (rotatedPoints[i] - other.points[i]).LengthFast;
			}

			return distance / count;
		}
	}
}
